01 January 2019

Generally, software applications are involved with security concerns. This means “Security” is a hot topic in software development and is a common part of all applications. If all software applications have a common part, called security, why don’t we hire a project/framework to meet this common part? Yes! To handle it, already many security frameworks, such as Apache Shiro, have been invented.

Apache Shiro is an extremely versatile framework, which covers many common/uncommon security requirements and meets various security requirements, from smallest mobile to the largest enterprise applications.

In this article, we will look at Apache Shiro and elaborate using it in an enterprise Spring application.

Apache Shiro introduces some basic and well-known security concepts such as User, Role, and Permission. An application resource (a web page for example) can be protected, so only authenticated users, or authorized users (authenticated users with specific roles/permissions) can access that resource.

Shiro needs to know two things to work; First user details, including user information and their roles/permissions. Second, the security policy, by which Shiro can determine which resource can be accessed by what users. For the first (Users, roles, and permissions), Shiro can load them from any data source, such as a file, database, or LDAP. Regardless of any data source, an Shiro object which is called Realm is responsible to load the details from the data source and provide them to the SecurityManager object. So the following code is used to load the user details from a data source and start up Apache Shiro.

Realm realm = …//create a Realm object corresponding to the data source
SecurityManager securityManager = new DefaultSecurityManager(realm);
SecurityUtils.setSecurityManager(securityManager);

The SecurityUtil class is a utility to interoperate with Shiro. In the code above, we only introduce the SecurityManager object to Shiro.

Now, Shiro has been started and we are ready to authenticate users. We use SecurityUtility to interact with Shiro, to login, logout, or know the logged in user details.

To authenticate users, we obtain the Subject object which specifies the authenticated user or an anonymous user if the user not authenticated yet.

Subject currentUser = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token.setRememberMe(true);
currentUser.login(token);

If the authentication is successful, the Subject object refers to the logged in user and can be used later to check who is authenticated, and if that user has a specific role(s)/permission(s).

if (currentUser.hasRole("technical")) {      
    log.info("You are a technical user!");             
}
if(currentUser.isPermitted("manager:ui")) {           
    log.info("You can manage the user interface!");                   
}

User Information Details

As already mentioned, Apache Shiro can load user details from any data source, including memory, file, and database. Connecting to a datasource (file, database,…), loading the user information, and provide it to SecurityManager is the responsibility of an object called Realm. Shiro provides some ready-to-use Realm classes to work with common datasource (file, database) which can be easily configured and used in Shiro applications. However, in the case of uncommon datasource or specific project circumstances, it’s still easy to implement and use your own Realm implementation.

The following class is a Realm implementation to use static user information in memory.

package com.ahmadsedi;

/**
 * @author Ahmad R. Seddighi (ahmadseddighi@yahoo.com)
 *         Date: 3/3/19
 *         Time: 10:45 AM
 */

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import java.util.*;

public class ProjectShiroRealm extends AuthorizingRealm {

    private Map<String, String> credentials = new HashMap<>();
    private Map<String, Set<String>> roles = new HashMap<>();
    private Map<String, Set<String>> perm = new HashMap<>();

    {
        credentials.put("user", "password");
        credentials.put("user2", "password2");
        credentials.put("user3", "password3");

        roles.put("user", new HashSet<>(Arrays.asList("admin")));
        roles.put("user2", new HashSet<>(Arrays.asList("editor")));
        roles.put("user3", new HashSet<>(Arrays.asList("author")));

        perm.put("admin", new HashSet<>(Arrays.asList("*")));
        perm.put("editor", new HashSet<>(Arrays.asList("articles:*")));
        perm.put("author",
                new HashSet<>(Arrays.asList("articles:compose",
                        "articles:save")));

    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {

        UsernamePasswordToken uToken = (UsernamePasswordToken) token;

        if(uToken.getUsername() == null
                || uToken.getUsername().isEmpty()
                || !credentials.containsKey(uToken.getUsername())
                ) {
            throw new UnknownAccountException("username not found!");
        }


        return new SimpleAuthenticationInfo(
                uToken.getUsername(), credentials.get(uToken.getUsername()),
                getName());
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        Set<String> roleNames = new HashSet<>();
        Set<String> permissions = new HashSet<>();

        principals.forEach(p -> {
                Set<String> roles = getRoleNamesForUser((String) p);
                roleNames.addAll(roles);
                permissions.addAll(getPermissions(roles));

        });

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
        info.setStringPermissions(permissions);
        return info;
    }

    protected Set<String> getRoleNamesForUser(String username)  {
        if(!roles.containsKey(username)) {
            throw new RuntimeException("username not found!");
        }

        return roles.get(username);
    }

    protected Set<String> getPermissions(Collection<String> roleNames)  {
        for (String role : roleNames) {
            if (!perm.containsKey(role)) {
                throw new RuntimeException("role not found!");
            }
        }

        Set<String> finalSet = new HashSet<>();
        for (String role : roleNames) {
            finalSet.addAll(perm.get(role));
        }

        return finalSet;
    }

}

This class extends AuthorizingRealm, and uses static data stored in three objects, credentials, roles, perm, which respectively represents users, users’ roles, and users’ permissions. The AuthrorizingRealm also overrides doGetAuthenticationInfo() and doGetAuthorizationInfo() methods which are responsible to authenticate users based on the token that receives, and authorize users based on their username. Instead of using static in memory data, users’ information can be put in a text file to be changed and maintained easily in the future without having to recompile the code. That file has a signature like is shown in the following.

[users]
user1 = password, admin
user2 = password2, manager
user3 = password3, technical
 
[roles]
admin = *
manager = manager:*
technical = manager:monitor, manager:ui

The first part, [users], has the user=password, role1, role2, …,roleN signature, which defines the username/password and roles of a specific user. The second part, [roles], has the role=permission1, permission2, …, permissionN signature, and defines which permissions are assigned to a specific role.

With the above user definitions, we use a IniRealm to load such that file and create a Realm object.

Realm iniRealm = new IniRealm("classpath:shiro.ini");
org.apache.shiro.mgt.SecurityManager securityManager = new DefaultSecurityManager(iniRealm);
SecurityUtils.setSecurityManager(securityManager);

Using Database instead of File

Today, most applications use database to provide dynamic user definition. In such these cases, we use ready-to-use JdbcRealm class instead of implementing a new one. So we change the shiro.ini file and insert there database connection properties as follows.

[main]
ds = com.mysql.jdbc.Driver
ds.serverName = localhost
ds.user = user
ds.password = password
ds.databaseName = db_name

jdbcRealm = org.apache.shiro.realm.jdbc.JdbcRealm
jdbcRealm.dataSource = $ds
jdbcRealm.permissionsLookupEnabled = true
jdbcRealm.authenticationQuery = "SELECT password FROM users WHERE user_name = ?"
jdbcRealm.userRolesQuery = "SELECT role_name FROM user_roles WHERE user_name = ?"
jdbcRealm.permissionsQuery = "SELECT permission FROM roles_permissions WHERE role_name = ?"

authc = org.apache.shiro.web.filter.authc.PassThruAuthenticationFilter
authc.loginUrl = /login/

[urls]
/login/ = authc

As you can see, we configured JdbcRealm to work with a mysql database with three database tables, users, user_roles, roles_permission. We provided three SQL statements to enable jdbcRealm object look up appropriate user, roles, and permissions when need it.

Full source code of this article can be found on my GitHub repository.